package ttot.plugin;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;

/**
 * <h1>ClasspathArchive</h1>
 * 
 * <h2>Usage</h2>
 * <dl>
 * <dt><code>new ClasspathArchive(List&lt;?&gt; classpathEntries)</code></dt>
 * <dd>Creates a new ClasspathArchive based on a list of directories and jar
 * files. The argument is untyped (<code>List&lt;?&gt;</code>) as to be
 * compatible with maven's <code>MavenProject.get*ClasspathElements()</code>
 * function. The elements in the list will be checked to be strings.</dd>
 * <dt><code>writeArchive(OutputStream outputStream)</code></dt>
 * <dd>Writes a jar file to the given output stream containing the contents of
 * the given directories and jar files. The jar file contents are uncompressed
 * before adding to the new archive.</dd>
 * <dt><code>getDuplicateEntries():HashSet&lt;String&gt;</code></dt>
 * <dd>When <code>writeArchive</code> completes this function returns the list
 * of duplicate entry names for files which could be found under the same name
 * from multiple sources in different jar archives or directories on the
 * classpath. Such files would have been added to the archive from only one (the
 * first encountered) source of the multiple possibile ones. Use
 * <code>getEntrySources</code> to investigate all the detected sources for a
 * given duplicate.
 * <p>
 * It is important to notice these duplicates as the most common cause of such a
 * situation is a class file (usually in different versions) found in multiple
 * libraries. As the situation itself is not a reason for failure upon creation
 * of a jar archive, <code>ClasspathArchive</code> provides this mechanism for
 * awareness purposes of the class user.
 * </p>
 * <p>
 * A general common false positive is directory entries (like when classes of
 * the same package are found in multiple jar files). You can distinguish these
 * entries as they end with a slash ('/').
 * </p>
 * <p>
 * These entries are not exactly file names (you will find nothing on the disk
 * under that name) but rather to names of files in the jar. Also note the path
 * separator here is platform independent, namely a slash ('/').
 * </p>
 * </dd>
 * <dt>getEntrySources(String entryName):List&lt;File&gt;</dt>
 * <dd>Returns the files or directories which contain an jar entry by the given
 * entry name (usually returned by <code>getDuplicateEntries</code>). The entry
 * name also includes the full path relative to the file container which
 * provided the given file, so the most common situation in which a duplicate
 * file is found is when there is a class which has the same name and package in
 * two or more sources.
 * <p>
 * The return value is a <code>List&lt;File&gt;</code>. The first entry is the
 * source from which the file was actually copied. If the entry name was
 * obtained from <code>getDuplicateEntries</code>, this list should contain at
 * least two elements.
 * </p>
 * </dd>
 * </dl>
 * 
 * @author george.aprozeanu@gmail.com
 * 
 */
public class ClasspathArchive {

	private List<String> classpathElements = new ArrayList<String>();

	private int total;

	private HashSet<String> entryNames = new HashSet<String>();

	private HashSet<File> addedFiles = new HashSet<File>();

	private HashMap<String, ArrayList<File>> entrySources = new HashMap<String, ArrayList<File>>();

	private HashSet<String> duplicateEntries = new HashSet<String>();

	public int getTotal() {
		return total;
	}

	private void addClasspathElement(String element) {
		classpathElements.add(element);
	}

	public ClasspathArchive(List<?> classpathElements) {
		for (Object classpathElement : classpathElements) {
			addClasspathElement((String) classpathElement);
		}
	}

	public void writeArchive(OutputStream outputStream) throws IOException {
		total = 0;
		JarOutputStream jos = new JarOutputStream(outputStream);
		putAllClasspathElements(jos);
		jos.close();
	}

	private void putAllClasspathElements(JarOutputStream jos)
			throws IOException {
		for (String classpathElement : classpathElements) {
			putClasspathElement(classpathElement, jos);
		}
	}

	private void putClasspathElement(String classpathElement,
			JarOutputStream jos) throws IOException {
		File f = new File(classpathElement);
		putFile(f, null, f, jos);
	}

	private String baseOffset(File base, File file) {
		String fullPath = file.getAbsolutePath();
		String basePath = base.getAbsolutePath();
		if (fullPath.startsWith(basePath)) {
			return fullPath.substring(basePath.length());
		}
		throw new IllegalArgumentException("Not a valid base (" + base
				+ ") for " + file);
	}

	/**
	 * Ace of Base
	 * 
	 * @param base
	 * @param file
	 * @return base
	 */
	private File baseOf(File base, File file) {
		if (file.isDirectory()) {
			return base != null ? base : file;
		}
		return base != null ? base : file.getParentFile();
	}

	private JarEntry entryFor(File base, File file) {
		String baseOffset = baseOffset(base, file);
		String entryName = File.pathSeparatorChar != '/' ? baseOffset.replace(
				File.pathSeparatorChar, '/') : baseOffset;
		return entryFor(entryName, file.lastModified());
	}

	private JarEntry entryFor(String entryName, long time) {
		JarEntry je = new JarEntry(entryName);
		je.setTime(time);
		return je;
	}

	private void dumpStream(OutputStream stream, InputStream inputStream)
			throws IOException {
		byte[] buffer = new byte[0x1000];
		int read = -1;
		while ((read = inputStream.read(buffer)) != -1) {
			total += read;
			stream.write(buffer, 0, read);
		}
	}

	public List<File> getEntrySources(String key) {
		return entrySources.get(key);
	}

	public HashSet<String> getDuplicateEntries() {
		return duplicateEntries;
	}

	private void putFile(File f, File base, File source, JarOutputStream jos)
			throws IOException {
		if (addedFiles.contains(f)) {
			throw new IllegalStateException(
					"Attempting to add a file twice to the archive: " + f);
		}
		addedFiles.add(f);
		base = baseOf(base, f);
		if (f.isDirectory()) {
			for (File sf : f.listFiles()) {
				putFile(sf, base, source, jos);
			}
		} else {
			if (f.getName().endsWith(".jar")) {
				putArchive(f, jos);
			} else {
				FileInputStream fis;
				putEntry(entryFor(base, f), (fis = new FileInputStream(f)),
						jos, source);
				fis.close();
			}
		}
	}

	private void putEntry(JarEntry je, InputStream inputStream,
			JarOutputStream jos, File source) throws IOException {
		String entryName = je.getName();
		ArrayList<File> entrySource = entrySources.get(entryName);
		if (entrySource == null) {
			entrySources.put(entryName, (entrySource = new ArrayList<File>()));
		}
		entrySource.add(source);
		if (entryNames.contains(entryName)) {
			duplicateEntries.add(entryName);
		} else {
			entryNames.add(entryName);
			jos.putNextEntry(je);
			dumpStream(jos, inputStream);
			jos.closeEntry();
		}
	}

	private void putArchive(File f, JarOutputStream jos)
			throws FileNotFoundException, IOException {
		JarInputStream jis = new JarInputStream(new FileInputStream(f));
		JarEntry inEntry;
		while ((inEntry = jis.getNextJarEntry()) != null) {
			JarEntry outEntry = entryFor(inEntry.getName(), inEntry.getTime());
			putEntry(outEntry, jis, jos, f);
			jis.closeEntry();
		}
		jis.close();
	}
}
